﻿using System;
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TextPioneer;
using System.Windows;

namespace TextPioneer.ResearchDataProvider
{
	#region TopicNode
	public class ProposalPaperTopicBundle : TopicBundle
	{
		private ProposalPaperDataProvider dataProvider;
		private TopicNode[] children = null;

		public ProposalPaperTopicBundle(int id, string name, int depth,
								double leadingFactor, Corpus corpus,
								DateTime start, DateTime end,
								ProposalPaperDataProvider dataProvider)
			: base(id, name, depth, leadingFactor, corpus, start, end)
		{
			this.dataProvider = dataProvider;
		}

		public override TopicNode[] GetChildren()
		{
			if(children == null)
			{
				var childIds = dataProvider.GetChildrenFromTree(this.Corpus.Id, this.Id);
				children = new TopicNode[childIds.Length];
				for (int i = 0; i < childIds.Length; i++)
				{
					children[i] = dataProvider.GetTopicNode(
						this.Corpus, childIds[i].Item1, childIds[i].Item2);
				}
			}
			return children;
		}
	}

	public class ProposalPaperTopic : Topic
	{
		private ProposalPaperDataProvider dataProvider;

		public ProposalPaperTopic(int id, string name, int depth,
										double leadingFactor, Corpus corpus,
										DateTime start, DateTime end,
										ProposalPaperDataProvider dataProvider)
			: base(id, name, depth, leadingFactor, corpus, start, end)
		{
			this.dataProvider = dataProvider;
		}

		public override int[] GetDocumentIds(DateTime startDate, DateTime endDate)
		{
			return dataProvider.GetDocumentIds(Corpus.Id, this.Id, startDate, endDate);
		}
	}
	#endregion

	#region Entity
	public class DocumentEntity : Entity
	{
		private int id;
		private string title;
		private object viewData;

		public override bool Equals(object obj)
		{
			if (obj is DocumentEntity)
				return this.id == (obj as DocumentEntity).id;
			else
				return false;
		}
		public override int GetHashCode()
		{
			return id;
		}

		public DocumentEntity(int id, string title, DateTime date, Topic topic)
		{
			this.id = id;
			this.title = title;
			this.viewData = null;
		}

		public override string Text
		{
			get { return title; }
			set { title = value; }
		}

		public override object ViewData
		{
			get { return viewData; }
			set { viewData = value; }
		}
	}

	public class ProgramEntity : Entity
	{
		private int id;
		private string name;
		private object viewData;

		public override bool Equals(object obj)
		{
			if (obj is ProgramEntity)
				return this.id == (obj as ProgramEntity).id;
			else
				return false;
		}
		public override int GetHashCode()
		{
			return id;
		}

		public ProgramEntity(int id, string name)
		{
			this.id = id;
			this.name = name;
			this.viewData = null;
		}

		public override string Text
		{
			get { return name; }
			set { name = value; }
		}

		public override object ViewData
		{
			get { return viewData; }
			set { viewData = value; }
		}
	}

	public class AuthorEntity : Entity
	{
		private int id;
		private string name;
		private object viewData;

		public override bool Equals(object obj)
		{
			if (obj is AuthorEntity)
				return this.id == (obj as AuthorEntity).id;
			else
				return false;
		}
		public override int GetHashCode()
		{
			return id;
		}

		public AuthorEntity(int id, string name)
		{
			this.id = id;
			this.name = name;
			this.viewData = null;
		}

		public override string Text
		{
			get { return name; }
			set { name = value; }
		}

		public override object ViewData
		{
			get { return viewData; }
			set { viewData = value; }
		}
	}
	#endregion

	#region DataProvider
	public class ProposalPaperDataProvider : IDataProvider
    {
		private DateTime startTime = DateTime.MaxValue, endTime = DateTime.MinValue;
		private Corpus[] corpora = null;

		public void Initialize(string dbPath)
		{
			dbAccessor = new DatabaseAccessor(dbPath);
		}

		public void Dispose()
		{
			dbAccessor.Dispose();
		}

		#region DataProviderInterface
		public DateTime StartTime
		{
			get
			{
				if (startTime != DateTime.MaxValue)
					return startTime;
				else
					return startTime = GetStartEndTime(true);
			}
		}

		public DateTime EndTime
		{
			get
			{
				if (endTime != DateTime.MinValue)
					return endTime;
				else
					return endTime = GetStartEndTime(false);
			}
		}

		public Corpus[] GetCorpora()
		{
			if (corpora == null)
				return corpora = GetDBCorpora();
			else
				return corpora;
		}

		public TopicBundle[] GetTopLevelTopics(Corpus corpus)
		{
			var fieldIds = GetFields();
			TopicBundle[] bundles = new TopicBundle[fieldIds.Length];
			for (int i = 0; i < fieldIds.Length; i++)
			{
				var bundleId = fieldIds[i] << 24;
				bundles[i] = (TopicBundle)GetTopicNode(corpus, bundleId, true);
			}
			return bundles;
		}

		public IEnumerable<LeadingTrendPoint> GetLeadingTrends(Topic topic1, Topic topic2)
		{
			if (topic1.Id != topic2.Id)
				throw new Exception("Topics are not in same topic.");
			if (topic1.Corpus == topic2.Corpus)
				throw new Exception("Topics cannot from the same corpus.");
			if (topic1.Corpus.IsBase)
				return GetLeadingTrends(topic1.Id, topic1.Corpus.Id, topic2.Corpus.Id);
			else if (topic2.Corpus.IsBase)
				return GetLeadingTrends(topic1.Id, topic2.Corpus.Id, topic1.Corpus.Id);
			else
				throw new Exception("No baseCorpus, cannot get LeadingTrends.");
		}

		public IEnumerable<DocumentSimilarityPair> GetSimilarityPairs(Topic topic1, Topic topic2)
		{
			if (topic1.Id != topic2.Id)
				throw new Exception("Topics are not in same topic.");
			if (topic1.Corpus == topic2.Corpus)
				throw new Exception("Topics cannot from the same corpus.");
			if (topic1.Corpus.IsBase)
				return GetSimilarityPairs(topic1.Id, topic1.Corpus.Id, topic2.Corpus.Id);
			else if (topic2.Corpus.IsBase)
				return GetSimilarityPairs(topic1.Id, topic2.Corpus.Id, topic1.Corpus.Id);
			else
				throw new Exception("No baseCorpus, cannot get LeadingTrends.");
		}

		public IEnumerable<Keyword> GetKeywords(TopicNode node, KeywordRanking ranking, int count)
		{
			if (ranking == KeywordRanking.TF)
			{
				if (node is TopicBundle)
					throw new Exception("Bundle does not have TF keyword table.");
				if (node.KeywordsTF.Count != count)
					node.KeywordsTF = GetTopicTFKeywords(node.Id, node.Corpus.Id, count).ToList();
				return node.KeywordsTF;
			}
			else if (ranking == KeywordRanking.TFIDF)
			{
				if (node.KeywordsTFIDF.Count != count)
					node.KeywordsTFIDF = GetTopicTFIDFKeywords(node.Id, node.Corpus.Id, count).ToList();
				return node.KeywordsTFIDF;
			}
			else
				throw new Exception("Ranking method error.");
		}

		public IEnumerable<EntityType> GetEntityTypes()
		{
			return new EntityType[]
			{
				new EntityType("Document", null),
				new EntityType("Author", null),
				new EntityType("Program", null),
			};
		}

		public IEnumerable<Entity> GetEntities(string typeName, Topic topic, DateTime date)
		{
			return GetTopicEntities(typeName, topic, date);
		}

		public IMoveEntityActivePointTask BeginMoveEntityActivePoint(Entity entity, ActivePoint originalPoint, Topic targetTopic)
		{
			throw new NotImplementedException();
		}
		#endregion

		#region DatabaseQuery

		private DatabaseAccessor dbAccessor;

		#region Basics
		internal DateTime GetStartEndTime(bool isStart)
		{
			string query = "";
			if (isStart)
				query = string.Format("select StartTime from DataTime;");
			else
				query = string.Format("select EndTime from DataTime;");
			using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
			{
				if (res.Read())
					return res.GetDateTime(0);
				else
					throw new Exception("Cannot find StartTime/EndTime in DataTime.");
			}
		}

		internal Corpus[] GetDBCorpora()
		{
			string query = string.Format("select * from Corpora;");
			using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
			{
				List<Corpus> corpora = new List<Corpus>();
				while (res.Read())
				{
					int id = res.GetInt32(0);
					string name = res.GetString(1);
					var corpus = new Corpus(id, name, (id == 0));
					corpora.Add(corpus);
				}
				return corpora.ToArray();
			}
		}

		internal TopicNode GetTopicNode(Corpus corpus, int nodeId, bool isBundle)
		{
			var query = string.Format(@"select * from CorpusTreeNode 
					where CorpusID={0} and NodeID={1} and IsBundle={2}",
									corpus.Id, nodeId, (isBundle == true)? 1:0);
			using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
			{
				if (res.Read())
				{
					var name = GetFieldName(nodeId >> 24);
					var size = res.GetInt32(3);
					var startTime = res.GetDateTime(4);
					var endTime = res.GetDateTime(5);
					var leadingFactor = res.GetDouble(6);
					if (isBundle)
					{
						name += "_bundle" + (nodeId & 0x00FFFFFF);
						var depth = (nodeId & 0x00FFFFFF) == 0 ? 0 : 1;
						return new ProposalPaperTopicBundle(nodeId, name, depth,
							leadingFactor, corpus, startTime, endTime, this);
					}
					else
					{
						name += "_cluster" + (nodeId & 0x00FFFFFF);
						return new ProposalPaperTopic(nodeId, name, 2,
							leadingFactor, corpus, startTime, endTime, this);
					}
				}
				else
					throw new Exception("Node does not exist.");
			}
		}

		internal Tuple<int, bool>[] GetChildrenFromTree(int corpusId, int parentId)
		{
			List<Tuple<int, bool>> children = new List<Tuple<int, bool>>();
			var query = string.Format(@"select * from DisplayTree
						where CorpusID={0} and ParentNodeId={1}",
						corpusId, parentId);
			using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
			{
				while (res.Read())
				{
					var childId = res.GetInt32(1);
					bool isChildBundle = res.GetBoolean(2);
					children.Add(new Tuple<int, bool>(childId, isChildBundle));
				}
				return children.ToArray();
			}
		}

		internal int[] GetFields()
		{
			var query = string.Format("select FieldID from Field;");
			using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
			{
				List<int> fieldIds = new List<int>();
				while (res.Read())
					fieldIds.Add(res.GetInt32(0));
				return fieldIds.ToArray();
			}
		}

		internal string GetFieldName(int fieldId)
		{
			string query = string.Format("select FieldName from Field where FieldID={0};", fieldId);
			using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
			{
				if (res.Read())
					return res.GetString(0);
				else
					throw new Exception("Field does not exist.");
			}
		}

		internal int[] GetDocumentIds(int corpusId, int topicId, DateTime startTime, DateTime endTime)
		{
			var fieldId = topicId >> 24;
			var clusterId = topicId & 0x00FFFFFF;
			var query = string.Format(@"select Document.DocumentID,Document.Timestamp from Document
										inner join ClusterDocument on Document.DocumentID=ClusterDocument.DocumentID
										where ClusterDocument.CorpusID={0} and 
										ClusterDocument.FieldID={1} and ClusterDocument.ClusterID={2};",
										corpusId, fieldId, clusterId);
			using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
			{
				List<int> docIds = new List<int>();
				while (res.Read())
				{
					var timestamp = res.GetDateTime(1);
					if (timestamp >= startTime && timestamp <= endTime)
						docIds.Add(res.GetInt32(0));
				}
				return docIds.ToArray();
			}
		}
		#endregion

		#region LeadLag
		internal IEnumerable<LeadingTrendPoint> GetLeadingTrends(int topicId, int baseCorpusId, int nonBaseCorpusId)
		{
			int fieldId = topicId >> 24;
			int clusterId = topicId & 0x00FFFFFF;
			var query = string.Format(@"select Timestamp,Value1,Value2 from LocalLeadLag
							where FieldID={0} and ClusterID={1} and CorpusID1={2} and CorpusID2={3}",
								fieldId, clusterId, baseCorpusId, nonBaseCorpusId);
			using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
			{
				List<LeadingTrendPoint> points = new List<LeadingTrendPoint>();
				while (res.Read())
				{
					DateTime time = res.GetDateTime(0);
					double v1 = res.GetDouble(1);
					double v2 = res.GetDouble(2);
					points.Add(new LeadingTrendPoint(time, v1, v2));
				}
				points.Sort((a, b) => { return a.Time.CompareTo(b.Time); });
				return points;
			}
		}

		internal IEnumerable<DocumentSimilarityPair> GetSimilarityPairs(int topicId, int baseCorpusId, int nonBaseCorpusId)
		{
			int fieldId = topicId >> 24;
			int clusterId = topicId & 0x00FFFFFF;
			var query = string.Format(@"select DocumentID1,DocumentID2,Similarity from DocumentMatrix
							where FieldID={0} and ClusterID={1} and CorpusID1={2} and CorpusID2={3}",
								fieldId, clusterId, baseCorpusId, nonBaseCorpusId);
			using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
			{
				List<DocumentSimilarityPair> pairs = new List<DocumentSimilarityPair>();
				while (res.Read())
				{
					int id1 = res.GetInt32(0);
					int id2 = res.GetInt32(1);
					double sim = res.GetDouble(2);
					pairs.Add(new DocumentSimilarityPair(id1, id2, sim));
				}
				return pairs;
			}
		}
		#endregion

		#region Keywords
		internal IEnumerable<Keyword> GetTopicTFKeywords(int topicId, int corpusId, int count)
		{
			var query = string.Format(@"select Keyword,Frequency from CorpusTreeNodeKeyword
							where NodeID={0} and IsBundle=0 and CorpusID={1} order by Frequency desc",
								topicId, corpusId);
			using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
			{
				List<Keyword> words = new List<Keyword>();
				while (res.Read())
				{
					words.Add(new Keyword(res.GetString(0), res.GetInt32(1)));
					if (words.Count >= count)
						break;
				}
				return words;
			}
		}

		internal IEnumerable<Keyword> GetTopicTFIDFKeywords(int topicId, int corpusId, int count)
		{
			var query = string.Format(@"select Keyword,Value from CorpusTreeNodeKeywordTFIDF
							where NodeID={0} and IsBundle=0 and CorpusID={1} order by Value desc",
								topicId, corpusId);
			int wordCount = 0;
			using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
			{
				while (res.Read())
				{
					yield return new Keyword(res.GetString(0), res.GetDouble(1));
					wordCount++;
					if (wordCount >= count)
						break;
				}
			}
		}
		#endregion

		#region Entity
		internal IEnumerable<Entity> GetTopicEntities(string typeName, Topic topic, DateTime date)
		{
			int topicId = topic.Id;
			int fieldId = topicId >> 24;
			int clusterId = topicId & 0x00FFFFFF;
			var docs = GetDocumentContent(clusterId, fieldId, topic.Corpus, date);
			
			List<Entity> entities = new List<Entity>();
			if (typeName == "Document")
			{
				GetDocumentEntities(topic, docs, entities);
			}
			else if (typeName == "Program")
			{
				if (topic.Corpus.Name == "proposal")
				{
					GetNSFProgramEntities(topic, docs, entities);
				}
				else if (topic.Corpus.Name == "paper")
				{
					GetVenueEntities(topic, docs, entities);
				}
			}
			else if (typeName == "Author")
			{
				Dictionary<int, List<AuthorDocInfo>> authorIds = new Dictionary<int, List<AuthorDocInfo>>();
				List<int> authors = new List<int>();
				List<int> ranks = new List<int>();
				foreach (var doc in docs)
				{
					GetDocAuthorWithRank(doc.DocumentID, authors, ranks);
					foreach (var author in authors)
					{
						if (!authorIds.ContainsKey(author))
							authorIds.Add(author, new List<AuthorDocInfo>());
						authorIds[author].Add(new AuthorDocInfo(doc.DocumentID, ranks[authors.IndexOf(author)], doc.CorpusID));
					}
				}
				foreach (var item in authorIds)
				{
					var authorId = item.Key;
					var authorName = GetAuthorNameById(authorId);
					int[] docIds = new int[item.Value.Count];
					int[] docRanks = new int[item.Value.Count];
					int[] corporaId = new int[item.Value.Count];
					for(int i = 0; i < item.Value.Count; i++)
					{
						docIds[i] = item.Value[i].DocumentID;
						docRanks[i] = item.Value[i].Rank;
						corporaId[i] = item.Value[i].CorpusID;
					}
					entities.Add(new AuthorEntity(item.Key, authorName));
				}
			}
			else
				throw new Exception("This type does not exist.");
			return entities;
		}

		private string GetAuthorNameById(int authorId)
		{
			var query = "select FirstName,LastName from Author where AuthorID=" + authorId;
			using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
			{
				if (res.Read())
				{
					return res.GetString(0) + " " + res.GetString(1);
				}
				else throw new Exception("Cannot find author by this ID");
			}
		}

		private void GetDocAuthorWithRank(int documentId, List<int> authors, List<int> ranks)
		{
			authors.Clear();
			ranks.Clear();
			var query = "select AuthorID,Rank from AuthorShip where DocumentID=" + documentId;
			using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
			{
				while (res.Read())
				{
					authors.Add(res.GetInt32(0));
					ranks.Add(res.GetInt32(1));
				}
			}
		}

		private void GetVenueEntities(Topic topic, TemporaryDoc[] docs, List<Entity> entities)
		{
			List<int> venuesId = new List<int>();
			foreach (var doc in docs)
			{
				if (!venuesId.Contains(doc.ProgramID))
					venuesId.Add(doc.ProgramID);
			}
			foreach (var venueId in venuesId)
			{
				var query = "select VenueName from Venue where VenueID=" + venueId;
				using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
				{
					if (res.Read())
						entities.Add(new ProgramEntity(venueId, res.GetString(0)));
				}
			}
		}

		private void GetNSFProgramEntities(Topic topic, TemporaryDoc[] docs, List<Entity> entities)
		{
			List<int> programIds = new List<int>();
			foreach (var doc in docs)
			{
				if (!programIds.Contains(doc.ProgramID))
					programIds.Add(doc.ProgramID);
			}
			foreach (var programId in programIds)
			{
				var query = "select NSFProgramName from NSFProgram where NSFProgramID=" + programId;
				using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
				{
					if (res.Read())
						entities.Add(new ProgramEntity(programId, res.GetString(0)));
				}
			}
		}

		private static void GetDocumentEntities(Topic topic, TemporaryDoc[] docs, List<Entity> entities)
		{
			foreach (var doc in docs)
			{
				entities.Add(new DocumentEntity(doc.DocumentID, doc.DocumentTitle, doc.Timestamp, topic));
			}
		}

		private TemporaryDoc[] GetDocumentContent(int clusterId, int fieldId, Corpus corpus, DateTime date)
		{
			var query = string.Format(@"select Document.DocumentID,Document.Title,Document.VenueID,Document.NSFProgramID,Document.Timestamp
								from Document
								inner join ClusterDocument on Document.DocumentID=ClusterDocument.DocumentID
								where ClusterDocument.FieldID={0} and ClusterDocument.ClusterID={1} and
								ClusterDocument.CorpusID={2};",
								fieldId, clusterId, corpus.Id);
			using (var res = dbAccessor.DBExecuteQuery(query, ResultSetOptions.None))
			{
				List<TemporaryDoc> docs = new List<TemporaryDoc>();
				while (res.Read())
				{
					var timestamp = res.GetDateTime(4);
					if (timestamp == date)
					{
						var id = res.GetInt32(0);
						var text = res.GetString(1);
						var vid = -1;
						if (corpus.Name == "proposal")
							vid = res.GetInt32(3);
						else if (corpus.Name == "paper")
							vid = res.GetInt32(2);
						docs.Add(new TemporaryDoc(id, text, corpus.Id, vid, timestamp));
					}
				}
				return docs.ToArray();
			}
		}

		#endregion

		#endregion
	}

	class TemporaryDoc
	{
		public int DocumentID { get; private set; }
		public string DocumentTitle { get; private set; }
		public int CorpusID { get; private set; }
		public int ProgramID { get; private set; }
		public DateTime Timestamp { get; private set; }
		public TemporaryDoc(int id, string title, int cid, int pid, DateTime time)
		{
			DocumentID = id;
			DocumentTitle = title;
			CorpusID = cid;
			ProgramID = pid;
			Timestamp = time;
		}
	}

	class AuthorDocInfo
	{
		public int DocumentID { get; private set; }
		public int Rank { get; private set; }
		public int CorpusID { get; private set; }
		public AuthorDocInfo(int id, int rank, int cid)
		{
			DocumentID = id;
			Rank = rank;
			CorpusID = cid;
		}
	}
	#endregion
}
